package cn.geminis.lowcode.compiler.code;

import cn.geminis.lowcode.compiler.Context;
import cn.geminis.lowcode.compiler.io.TreeFileObject;
import cn.geminis.lowcode.compiler.util.JdkCompilerClass;
import cn.geminis.lowcode.compiler.util.JdkTreeMakerUtils;
import cn.geminis.lowcode.utils.Utils;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import lombok.Cleanup;
import lombok.Getter;
import lombok.NonNull;
import lombok.SneakyThrows;

import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

/**
 * @author puddi
 */
public class LcClass extends AbstractCode<ClassTree, TypeElement> {

    @Getter
    private final LcPackage packagee;
    private final LcModifier modifiers;
    private final String name;
    private final String[] implementing;
    private final boolean needGenerateSourceFile;

    public LcClass(Element element) {
        super(element);
        this.packagee = LcPackage.ensure(Utils.getElementPackageName(element));
        this.name = element.getSimpleName().toString();
        this.modifiers = null;
        this.implementing = null;
        this.needGenerateSourceFile = false;
    }

    public LcClass(@NonNull LcPackage pkg, @NonNull LcModifier modifiers, @NonNull String name, String... implementing) {
        this.packagee = pkg;
        this.modifiers = modifiers;
        this.name = name;
        this.implementing = implementing;
        this.needGenerateSourceFile = true;
    }

    public List<LcVariable> getFields() {
        var result = new ArrayList<LcVariable>();
        result.addAll(
                this.getJdkElement().getEnclosedElements().stream()
                        .filter(member -> member instanceof VariableElement)
                        .map(field -> new LcVariable((VariableElement) field))
                        .toList()
        );
        result.addAll(
                this.getJdkTree().getMembers().stream()
                        .filter(
                                member -> member instanceof VariableTree field &&
                                        result.stream().noneMatch(exist -> exist.getName().equals(field.getName().toString()))
                        )
                        .map(field -> new LcVariable((VariableTree) field))
                        .toList()
        );

        return result;
    }

    @Override
    protected ClassTree createJdkTree() {
        return JdkTreeMakerUtils.classDef(this.modifiers.getJdkTree(), this.name, this.implementing);
    }

    @Override
    protected TypeElement createJdkElement() {
        var classTree = this.getJdkTree();
        var compilationUnit = createCompilationUnit();
        var enter = Context.INSTANCE.getEnter();
        var compilationUnitEnv = Utils.callMethod(enter, "topLevelEnv", compilationUnit);

        Utils.callMethod(enter, "main", Utils.listFrom(List.of(compilationUnit)));

        var compiler = Context.INSTANCE.getJavaCompiler();
        var classEnv = Utils.callMethod(enter, "classEnv", classTree, compilationUnitEnv);
        Utils.callMethod(compiler, "attribute", classEnv);

        classTree = Utils.callMethodWithTypes(
                Context.INSTANCE.getLambdaToMethod(),
                "translateTopLevelClass",
                new Class[]{
                        JdkCompilerClass.ENV,
                        JdkCompilerClass.JC_TREE,
                        JdkCompilerClass.TREE_MAKER
                },
                classEnv,
                classTree,
                Context.INSTANCE.getTreeMaker()
        );

        Utils.callMethod(Context.INSTANCE.getGen(), "genClass", compilationUnitEnv, classTree);

        var element = (TypeElement) Utils.getField(classTree, "sym");
        Utils.setField(element, "sourcefile", null);
        return element;
    }

    public void generateClassFile() {
        var element = this.getJdkElement();
        Utils.callMethodWithTypes(
                Context.INSTANCE.getClassWriter(),
                "writeClass",
                new Class[]{
                        JdkCompilerClass.CLASS_SYMBOL
                },
                element
        );
    }

    @SneakyThrows
    public void generateSourceFile() {
        if (this.needGenerateSourceFile) {
            var file = Context.INSTANCE.getProcessingEnv().getFiler().createSourceFile(this.packagee.getJdkTree().getPackageName().toString() + "." + this.name);
            @Cleanup var writer = file.openWriter();
            writer.write(this.createCompilationUnit().toString());
        }
    }

    public void append(AbstractCode<?, ?> newElement) {
        var jdkTree = this.getJdkTree();
        JdkTreeMakerUtils.setPos(this);
        var defs = Utils.getField(jdkTree, "defs");
        var newDefs = Utils.callMethodWithTypes(
                defs,
                "append",
                new Class[]{
                        Object.class
                },
                newElement.getJdkTree()
        );
        Utils.setField(jdkTree, "defs", newDefs);
        super.treeChanged();
    }

    public void prepend(AbstractCode<?, ?> newElement) {
        var jdkTree = this.getJdkTree();
        JdkTreeMakerUtils.setPos(this);
        var defs = Utils.getField(jdkTree, "defs");
        var newDefs = Utils.callMethodWithTypes(
                defs,
                "prepend",
                new Class[]{
                        Object.class
                },
                newElement.getJdkTree()
        );
        Utils.setField(jdkTree, "defs", newDefs);
        super.treeChanged();
    }

    public void appendAnnotation(LcAnnotation annotation) {
        var jdkTree = this.getJdkTree();
        JdkTreeMakerUtils.setPos(this);
        var modifier = Utils.getField(jdkTree, "mods");
        var annotations = Utils.<List<Tree>>getField(modifier, "annotations");
        var newAnnotations = Utils.callMethodWithTypes(
                annotations,
                "append",
                new Class[]{
                        Object.class
                },
                annotation.getJdkTree()
        );
        Utils.setField(modifier, "annotations", newAnnotations);
        super.treeChanged();
    }

    public void appendImplementing(String implement) {
        var jdkTree = this.getJdkTree();
        JdkTreeMakerUtils.setPos(this);
        var implementing = Utils.getField(jdkTree, "implementing");
        var newImplementing = Utils.callMethodWithTypes(
                implementing,
                "append",
                new Class[]{
                        Object.class
                },
                JdkTreeMakerUtils.select(implement)
        );
        Utils.setField(jdkTree, "implementing", newImplementing);
        super.treeChanged();
    }

    public void remove(String name) {
        var jdkTree = this.getJdkTree();
        var defs = Utils.getField(jdkTree, "defs");
        var defArray = (Object[]) Utils.callMethodWithTypes(
                defs,
                "toArray",
                null
        );
        defArray = Stream.of(defArray).filter(def -> !Utils.getField(def, "name").toString().equals(name)).toArray();

        var newDefs = Utils.callMethodWithTypes(
                defs.getClass(),
                "from",
                new Class[]{
                        defArray.getClass()
                },
                new Object[]{defArray}
        );
        Utils.setField(jdkTree, "defs", newDefs);
        super.treeChanged();
    }

    private Object createCompilationUnit() {
        var classTree = this.getJdkTree();
        JdkTreeMakerUtils.setPos(this);
        var treeMaker = Context.INSTANCE.getTreeMaker();
        var compilationUnit = Utils.callMethod(
                treeMaker,
                "TopLevel",
                Utils.listFrom(List.of(
                        this.packagee.getJdkTree(),
                        classTree
                ))
        );
        Utils.setField(compilationUnit, "sourcefile", new TreeFileObject(classTree));
        var symtab = Context.INSTANCE.getSymtab();
        Utils.setField(compilationUnit, "packge", this.packagee.getJdkElement());
        Utils.setField(compilationUnit, "modle", Utils.getField(symtab, "unnamedModule"));
        return compilationUnit;
    }

    public boolean existAnnotation(String annotationType) {
        var element = this.getJdkElement();
        if (element == null) {
            return this.getJdkTree().getModifiers().getAnnotations().stream()
                    .anyMatch(annotation -> annotation.getAnnotationType().toString().equals(annotationType));
        } else {
            return element.getAnnotationMirrors().stream().anyMatch(
                    annotation -> annotation.getAnnotationType().toString().equals(annotationType)
            );
        }
    }
}
